package com.jiangyg.mall.gateway.support.security;

import com.jiangyg.mall.authz.constant.AuthzConstant;
import com.jiangyg.mall.authz.feign.AuthzFeignService;
import com.jiangyg.mall.core.support.restful.Result;
import com.jiangyg.mall.core.utils.Assert;
import com.jiangyg.mall.core.utils.JsonUtils;
import com.jiangyg.mall.core.utils.Logger;
import com.jiangyg.mall.core.utils.WebUtils;
import com.jiangyg.mall.gateway.support.feign.FeignRequestHolder;
import com.jiangyg.mall.gateway.support.route.RouteHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;

/**
 * 类描述：访问权限
 *
 * @author jiangyg
 * @date 2022-01-09
 */
@Slf4j
@Component
public class AccessAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    /**
     * 路由处理规则
     */
    private final RouteHandler routeHandler;

    private final AuthzFeignService authzFeignService;

    @Autowired
    public AccessAuthorizationManager(RouteHandler routeHandler,
                                      AuthzFeignService authzFeignService) {
        Assert.notNull(routeHandler);
        this.routeHandler = routeHandler;
        this.authzFeignService = authzFeignService;
    }

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext context) {
        try {
            final ServerWebExchange exchange = context.getExchange();
            // 1. 预检请求直接放行
            final ServerHttpRequest request = exchange.getRequest();
            if (request.getMethod() == HttpMethod.OPTIONS) {
                return Mono.just(new AuthorizationDecision(true));
            }
            FeignRequestHolder.set(request);
            // 2. 获取网关请求路径
            final String path = WebUtils.getRequestPath(exchange);
            // 3. 获取真实转发路径，然后进行权限校验
            final String realPath = routeHandler.realPath(path);
            final Map<String, Object> attachParams = obtainRequestAttachParams(request);
            final Result<String> authentication = authzFeignService.authentication(realPath, attachParams);
            // 4. 校验失败则直接返回拒接访问
            Logger.info(log, () -> String.format("[访问权限校验]-请求[%s]验证结果[%s]", path, JsonUtils.toJSONString(authentication)));
            if (!authentication.success()) {
                return Mono.just(new AuthorizationDecision(false));
            }
            // 5. 请求头追加用户认证信息
            if (StringUtils.isNotBlank(authentication.getBody())) {
                request.mutate().header(AuthzConstant.AUTH_USER_INFO, authentication.getBody());
            }
            // 6. 校验成功
            return Mono.just(new AuthorizationDecision(true));
        } finally {
            FeignRequestHolder.clear();
        }
    }

    /**
     * 功能描述：获取请求附加参数
     *
     * @param request 请求
     * @return Map<String, Object> 请求附加参数
     */
    private Map<String, Object> obtainRequestAttachParams(ServerHttpRequest request) {
        final Map<String, Object> m = new HashMap<>(16);
        // 1. 解析请求头
        final HttpHeaders headers = request.getHeaders();
        headers.forEach((k, v) -> {
            if (CollectionUtils.isNotEmpty(v)) {
                m.put(k, v.get(0));
            }
        });
        // 2. 解析 cookie
        final Map<String, HttpCookie> cookies = request.getCookies().toSingleValueMap();
        cookies.forEach((k, v) -> m.put(v.getName(), v.getValue()));
        return m;
    }

}
